This is an R Markdown Notebook

Lecture 26 - Your project. Deep Learning with H2O. P.2 Build Deep Learning Model for our example

Deep Learning… This lecture is dedicated to the implementation of Deep Learning models into for our Data

Work overview (from previous lecture)

  • re-arranging data as matrix
  • fitting deep learning models
  • testing the models
  • saving models
  • implementation in our ShinyApp…

The idea will be to use one particular feature e.g. Tubing Process, Impedance to play with. We will not yet implement this in Shiny but rather focus to prepare our data, fit and test the model

Re-arranging data as a matrix

Let’s get our time - series data as a dataframe first…

library(tidyverse)
library(plotly)
# ============= READ DATA =================
# Read our small data ... 
DF_Data_Recent <- readRDS("DF_Data_Process_Recent.data") 
DF_Equipm <- read_csv("DF_EquipmData.csv")
# data frame containing Event Names
DF_EvCode <- read_csv("DF_EvCodeDataProject.csv")
# Data manipulation and saving to the DF_TEMP
DF_TEMP <- DF_Data_Recent %>% 
  # join to decode equipment serial number
  inner_join(DF_Equipm, by = "IDEquipment") %>% 
  # join to decode Event Code meaning
  inner_join(DF_EvCode, by = "EventCode") %>% 
  # select only column needed
  select(StartDate, Name, AnalogVal, EventText)

Then I will plot my data for all 4 machines as just to remember how it looks like…

# creating human readable data and visualize them
DF_TEMP %>% 
  filter(EventText == "Tubing Process, resistance Ohm") %>% 
  ggplot(aes(x = StartDate, y = AnalogVal, col = Name)) + geom_point()+facet_grid(~Name)

Looking on the chart above I can see that machine #1 seems to be the best. Let’s assume that I take this for granted using my ‘domain’ knowledge. In case you just a consultant or just a Data Scientist, you may want to find and consult this ‘person’. For this reason, I will take that data from Machine #1 as a reference to build my Deep Learning Model.

Following chunk of code will extract this dataset with a filter() function.

# extracting only one machine
DF_M1 <- DF_TEMP %>% 
  filter(EventText == "Tubing Process, resistance Ohm") %>% 
  filter(Name == "Machine #1") %>% 
  select(StartDate, AnalogVal) %>% 
  arrange(StartDate) 
head(DF_M1)

Transposing the data

Now we need to transpose our data from long to wide structure and to fit it to matrix! For the moment, we will ‘forget’ the StartDate values and simply parse the values into matrix of dimension say 150 columns and 20 rows… Of course one need to recover some basic R skills for that :) if not use stackoverflow… (How to turn a vector into a matrix in R?)[https://stackoverflow.com/questions/14614946/how-to-turn-a-vector-into-a-matrix-in-r]

But in order to make things more clear for you (and for me) I will start simple. I will limit the output to 50 pieces of values to have more clear understanding on the process we do.

NOTE: Play with the code yourself to better understand every step!

DF_M1 <- DF_TEMP %>% 
  filter(EventText == "Tubing Process, resistance Ohm") %>% 
  filter(Name == "Machine #1") %>% 
  arrange(StartDate) %>% 
  select(AnalogVal) %>% 
  head(50) %>% 
  t() %>%  # this brings us a matrix
  matrix(nrow = 5, ncol = 10, byrow = TRUE) # transforming that into matrix size 5rows and 10columns
DF_M1
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,]   50   44   43   49   43   50   44   50   44    50
[2,]   44   51   45   51   43   50   44   50   44    50
[3,]   43   49   43   49   45   51   44   51   45    51
[4,]   44   43   49   43   49   43   49   43   52    45
[5,]   51   44   50   44   50   44   45   51   44    50

Notice to set byrow argument to TRUE. This way we will keep track on how our data are populated. Every row will contain about 150 observations of the Time-Series data. This will be equivalent of known time period.

Coming back to our data. Let’s complete this procedure for entire dataset. Let’s use the recipe to do a final transformation:

DF_M1 <- DF_TEMP %>% 
  filter(EventText == "Tubing Process, resistance Ohm") %>% 
  filter(Name == "Machine #1") %>% 
  arrange(StartDate) %>% 
  select(AnalogVal) %>% 
  head(3000) %>% 
  t() %>%  # this brings us a matrix
  matrix(nrow = 20, ncol = 150, byrow = TRUE) # transforming that into matrix size 20x150

Wonderful, let’s try to see! our new object as an image!!!

Explore the matrix as a surface!

Let’s use plotly 3D graph to explore what we have got!

plot_ly(z = DF_M1, type = "surface")
Values from Machine 1 used for Training

Values from Machine 1 used for Training

This should be good enough to fit our deep learning model.

Generalize data preparation as a function

As mr Bill Gates once said: “I would rather hire a lazy person as he/she will find the easiest way to solve a problem…” we must be lazy! For that I will already now think about writing a function that will ‘prepare’ my test datasets. Take a moment to study this:

# build function converting time series data to matrix
to_matrix <- function(x, filter_Event, filter_Machine, n_cols) {
  ### PURPOSE: Transform Time Series Column of the dataframe to the matrix
  #            with specified number of columns. Number of rows will be automatically
  #            found and remaining data points discarded
  # # Uncomment variable to debug function
  # filter_Event <- "Tubing Process, resistance Ohm"
  # filter_Machine <- "Machine #4"
  # x <- DF_TEMP
  # n_cols <- 150
  
  # get intermediate object and dimension
  Step1 <- x %>% 
    filter(EventText == filter_Event) %>% 
    filter(Name == filter_Machine) %>% 
    arrange(StartDate) %>% 
    select(AnalogVal)
  # find number of rows of data frame
  nrows <- Step1 %>% nrow()
  # find the number of row in a matrix (Whole Rows), the value will have decimals...
  WN <- nrows/n_cols
  ## extract the whole number uncomment for debug/test
  # WN <- 19.2
  # WN <- 19.8
  if((WN - round(WN)) < 0){WN <- round(WN) - 1} else {WN <- round(WN)}
  # find number of rows to extract data
  n <- n_cols * WN
  # extract relevant matrix
  Step2 <- Step1 %>% 
    head(n) %>% #only use whole number to avoid errors
    t() %>%  # this brings us a matrix
    matrix(nrow = WN, ncol = n_cols, byrow = TRUE) # transforming that into matrix size 20x150
  # return the result of the function
  return(Step2)
}

This function can definitely be easily reused for our ShinyApp. It can be used to generate dataset for any other machine event. Code snippets below will use this function to generate ‘Test’ datasets.

A. For machine 2:

DF_M2 <- to_matrix(DF_TEMP, 
                   filter_Event = "Tubing Process, resistance Ohm",
                   filter_Machine = "Machine #2", 
                   n_cols = 150)

B. For machine 3:

DF_M3 <- to_matrix(DF_TEMP, 
                   filter_Event = "Tubing Process, resistance Ohm",
                   filter_Machine = "Machine #3", 
                   n_cols = 150)

C. For machine 4:

DF_M4 <- to_matrix(DF_TEMP, 
                   filter_Event = "Tubing Process, resistance Ohm",
                   filter_Machine = "Machine #4", 
                   n_cols = 150)

And we can also visualize that to confront:

For Machine 2

plot_ly(z = DF_M2, type = "surface")
Values from Machine 2 used for Test

Values from Machine 2 used for Test

For Machine 3 - our candidate for anomaly detection!

plot_ly(z = DF_M3, type = "surface")
Values from Machine 3 used for Test

Values from Machine 3 used for Test

For Machine 4 - Another candidate for anomaly detection!

plot_ly(z = DF_M4, type = "surface")
Values from Machine 4 used for Test

Values from Machine 4 used for Test

Fitting the Deep Learning Model

Launch the deep learning machine again…

# to load the library
library(h2o)
# to initialize the 'machine'
localH2O = h2o.init()

H2O is not running yet, starting it now...

Note:  In case of errors look at the following log files:
    C:\Users\fxtrams\AppData\Local\Temp\RtmpwjnYF6/h2o_fxtrams_started_from_r.out
    C:\Users\fxtrams\AppData\Local\Temp\RtmpwjnYF6/h2o_fxtrams_started_from_r.err

java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

Starting H2O JVM and connecting: . Connection successful!

R is connected to the H2O cluster: 
    H2O cluster uptime:         2 seconds 104 milliseconds 
    H2O cluster version:        3.14.0.7 
    H2O cluster version age:    24 days  
    H2O cluster name:           H2O_started_from_R_fxtrams_pfu905 
    H2O cluster total nodes:    1 
    H2O cluster total memory:   1.77 GB 
    H2O cluster total cores:    4 
    H2O cluster allowed cores:  4 
    H2O cluster healthy:        TRUE 
    H2O Connection ip:          localhost 
    H2O Connection port:        54321 
    H2O Connection proxy:       NA 
    H2O Internal Security:      FALSE 
    H2O API Extensions:         Algos, AutoML, Core V3, Core V4 
    R Version:                  R version 3.2.5 (2016-04-14) 

Load datasets to H2O

Then we will download the datasets into h2o. Remember H2O is just operated from R but it’s a computer besides!

# Import train data into the H2O cluster
train_M1 <- as.h2o(x = DF_M1, destination_frame = "train_M1")

  |                                                                                                                      
  |                                                                                                                |   0%
  |                                                                                                                      
  |================================================================================================================| 100%
# Also import our test datasets for Machines 2 and 3...
test_M2  <- as.h2o(x = DF_M2, destination_frame = "test_M2")

  |                                                                                                                      
  |                                                                                                                |   0%
  |                                                                                                                      
  |================================================================================================================| 100%
test_M3  <- as.h2o(x = DF_M3, destination_frame = "test_M3")

  |                                                                                                                      
  |                                                                                                                |   0%
  |                                                                                                                      
  |================================================================================================================| 100%
test_M4  <- as.h2o(x = DF_M4, destination_frame = "test_M4")

  |                                                                                                                      
  |                                                                                                                |   0%
  |                                                                                                                      
  |================================================================================================================| 100%

Building Deep Learning model with autoencoder

Now, once we know how our data looks like we can start to do our Anomaly Model.

# Train deep autoencoder learning model on "normal" 
# training data, y ignored 
normality_model <- h2o.deeplearning(
 x = names(train_M1), 
 training_frame = train_M1, 
 activation = "Tanh", 
 autoencoder = TRUE, 
 hidden = c(50,20,50), 
 sparse = TRUE,
 l1 = 1e-4, 
 epochs = 100)

  |                                                                                                                      
  |                                                                                                                |   0%
  |                                                                                                                      
  |================================================================================================================| 100%

Calculate MSE from the train dataset

Let’s use this model on our training dataset…

# computer error of the model
h2o.anomaly(normality_model, train_M1) %>% as.data.frame() %>% plot.ts(ylim = c(0, 10), type = "p")

Getting to know what AI see:

# visually see it
test_recon_M1 <- h2o.predict(normality_model, train_M1) %>% as.matrix()

  |                                                                                                                      
  |                                                                                                                |   0%
  |                                                                                                                      
  |================================================================================================================| 100%
plot_ly(z = test_recon_M1, type = "surface")

Detect Anomaly using MSE value

Now we will try to use test datasets from machine 2.

# Compute reconstruction error with the Anomaly 
# detection app (MSE between output and input layers)
h2o.anomaly(normality_model, test_M2) %>%  as.data.frame() %>% plot.ts(ylim = c(0, 10), type = "p")

What a mess you would say - all observations are anomalous?? Yes! In fact this is correct. Indeed, sometimes AI lead to such a discoveries… In reality we observe that Machine #2 is completely different from Machine #1…

And for Machine 3

h2o.anomaly(normality_model, test_M3) %>%  as.data.frame() %>% plot.ts(ylim = c(0, 10), type = "p")

Machine 3 has only anomalies at certain periods. There are general good performance while at approximately 10% of time we have a periods of anomalous behaviour…

Machine 4 You may try to practice with Machine 4!

h2o.anomaly(normality_model, test_M4) %>%  as.data.frame() %>% plot.ts(ylim = c(0, 10), type = "p")

Machine 4 shows anomalies only at periods 5 and 8. Then the behavious stabilizes…

Saving Deep Learning Model for the future use

To use our model in our ShinyApp we will save it…

if(!file.exists("www/tmp/normality_model.bin")){
h2o.saveModel(normality_model, "www/tmp/normality_model.bin")
h2o.download_pojo(normality_model, "www/tmp", get_jar = TRUE)
}

And let’s not forget to switch off our cluster!

h2o.shutdown(prompt= FALSE)
[1] TRUE

Conclusion

in this lecture we have performed:

  • Time-Series data transformation from DataFrame to Matrix
  • Trained Autoencoder model on ‘best’ performing machine
  • Model was tested on a ‘worse’ performing machine
  • Model was saved in several formats for the further use

Next step

our next step will be to repeat the procedure but on our machine data.

  • re-arranging data as matrix
  • fitting deep learning models
  • testing the models
  • saving models
  • implementation in our ShinyApp…
LS0tDQp0aXRsZTogIkxlY3R1cmUgMjYgLSBJbnRvIERlZXAgTGVhcm5pbmcgUDIiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IGRlZmF1bHQNCiAgaHRtbF9ub3RlYm9vazogZGVmYXVsdA0KICBwZGZfZG9jdW1lbnQ6IGRlZmF1bHQNCi0tLQ0KDQpUaGlzIGlzIGFuIFtSIE1hcmtkb3duXShodHRwOi8vcm1hcmtkb3duLnJzdHVkaW8uY29tKSBOb3RlYm9vaw0KDQoNCiMjIyBMZWN0dXJlIDI2IC0gWW91ciBwcm9qZWN0LiBEZWVwIExlYXJuaW5nIHdpdGggSDJPLiBQLjIgQnVpbGQgRGVlcCBMZWFybmluZyBNb2RlbCBmb3Igb3VyIGV4YW1wbGUNCg0KRGVlcCBMZWFybmluZy4uLiBUaGlzIGxlY3R1cmUgaXMgZGVkaWNhdGVkIHRvIHRoZSBpbXBsZW1lbnRhdGlvbiBvZiBEZWVwIExlYXJuaW5nIG1vZGVscyBpbnRvIGZvciBvdXIgRGF0YQ0KDQojIyMjIFdvcmsgb3ZlcnZpZXcgKGZyb20gcHJldmlvdXMgbGVjdHVyZSkNCg0KKiByZS1hcnJhbmdpbmcgZGF0YSBhcyBtYXRyaXgNCiogZml0dGluZyBkZWVwIGxlYXJuaW5nIG1vZGVscw0KKiB0ZXN0aW5nIHRoZSBtb2RlbHMNCiogc2F2aW5nIG1vZGVscw0KKiBpbXBsZW1lbnRhdGlvbiBpbiBvdXIgU2hpbnlBcHAuLi4NCg0KVGhlIGlkZWEgd2lsbCBiZSB0byB1c2Ugb25lIHBhcnRpY3VsYXIgZmVhdHVyZSBlLmcuICoqVHViaW5nIFByb2Nlc3MsIEltcGVkYW5jZSoqIHRvIHBsYXkgd2l0aC4gV2Ugd2lsbCBub3QgeWV0IGltcGxlbWVudCB0aGlzIGluIFNoaW55IGJ1dCByYXRoZXIgZm9jdXMgdG8gcHJlcGFyZSBvdXIgZGF0YSwgZml0IGFuZCB0ZXN0IHRoZSBtb2RlbA0KDQojIyMjIFJlLWFycmFuZ2luZyBkYXRhIGFzIGEgbWF0cml4DQoNCkxldCdzIGdldCBvdXIgdGltZSAtIHNlcmllcyBkYXRhIGFzIGEgZGF0YWZyYW1lIGZpcnN0Li4uDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KHBsb3RseSkNCg0KIyA9PT09PT09PT09PT09IFJFQUQgREFUQSA9PT09PT09PT09PT09PT09PQ0KIyBSZWFkIG91ciBzbWFsbCBkYXRhIC4uLiANCkRGX0RhdGFfUmVjZW50IDwtIHJlYWRSRFMoIkRGX0RhdGFfUHJvY2Vzc19SZWNlbnQuZGF0YSIpIA0KDQpERl9FcXVpcG0gPC0gcmVhZF9jc3YoIkRGX0VxdWlwbURhdGEuY3N2IikNCiMgZGF0YSBmcmFtZSBjb250YWluaW5nIEV2ZW50IE5hbWVzDQpERl9FdkNvZGUgPC0gcmVhZF9jc3YoIkRGX0V2Q29kZURhdGFQcm9qZWN0LmNzdiIpDQoNCiMgRGF0YSBtYW5pcHVsYXRpb24gYW5kIHNhdmluZyB0byB0aGUgREZfVEVNUA0KREZfVEVNUCA8LSBERl9EYXRhX1JlY2VudCAlPiUgDQogICMgam9pbiB0byBkZWNvZGUgZXF1aXBtZW50IHNlcmlhbCBudW1iZXINCiAgaW5uZXJfam9pbihERl9FcXVpcG0sIGJ5ID0gIklERXF1aXBtZW50IikgJT4lIA0KICAjIGpvaW4gdG8gZGVjb2RlIEV2ZW50IENvZGUgbWVhbmluZw0KICBpbm5lcl9qb2luKERGX0V2Q29kZSwgYnkgPSAiRXZlbnRDb2RlIikgJT4lIA0KICAjIHNlbGVjdCBvbmx5IGNvbHVtbiBuZWVkZWQNCiAgc2VsZWN0KFN0YXJ0RGF0ZSwgTmFtZSwgQW5hbG9nVmFsLCBFdmVudFRleHQpDQpgYGANCg0KVGhlbiBJIHdpbGwgcGxvdCBteSBkYXRhIGZvciBhbGwgNCBtYWNoaW5lcyBhcyBqdXN0IHRvIHJlbWVtYmVyIGhvdyBpdCBsb29rcyBsaWtlLi4uDQoNCmBgYHtyfQ0KIyBjcmVhdGluZyBodW1hbiByZWFkYWJsZSBkYXRhIGFuZCB2aXN1YWxpemUgdGhlbQ0KREZfVEVNUCAlPiUgDQogIGZpbHRlcihFdmVudFRleHQgPT0gIlR1YmluZyBQcm9jZXNzLCByZXNpc3RhbmNlIE9obSIpICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gU3RhcnREYXRlLCB5ID0gQW5hbG9nVmFsLCBjb2wgPSBOYW1lKSkgKyBnZW9tX3BvaW50KCkrZmFjZXRfZ3JpZCh+TmFtZSkNCmBgYA0KDQpMb29raW5nIG9uIHRoZSBjaGFydCBhYm92ZSBJIGNhbiBzZWUgdGhhdCBtYWNoaW5lICMxIHNlZW1zIHRvIGJlIHRoZSBiZXN0LiBMZXQncyBhc3N1bWUgdGhhdCBJIHRha2UgdGhpcyBmb3IgZ3JhbnRlZCB1c2luZyBteSAnZG9tYWluJyBrbm93bGVkZ2UuIEluIGNhc2UgeW91IGp1c3QgYSBjb25zdWx0YW50IG9yIGp1c3QgYSBEYXRhIFNjaWVudGlzdCwgeW91IG1heSB3YW50IHRvIGZpbmQgYW5kIGNvbnN1bHQgdGhpcyAncGVyc29uJy4gRm9yIHRoaXMgcmVhc29uLCBJIHdpbGwgdGFrZSB0aGF0IGRhdGEgZnJvbSBNYWNoaW5lICMxIGFzIGEgcmVmZXJlbmNlIHRvIGJ1aWxkIG15IERlZXAgTGVhcm5pbmcgTW9kZWwuIA0KDQpGb2xsb3dpbmcgY2h1bmsgb2YgY29kZSB3aWxsIGV4dHJhY3QgdGhpcyBkYXRhc2V0IHdpdGggYSAqKmZpbHRlcigpKiogZnVuY3Rpb24uIA0KDQpgYGB7cn0NCiMgZXh0cmFjdGluZyBvbmx5IG9uZSBtYWNoaW5lDQpERl9NMSA8LSBERl9URU1QICU+JSANCiAgZmlsdGVyKEV2ZW50VGV4dCA9PSAiVHViaW5nIFByb2Nlc3MsIHJlc2lzdGFuY2UgT2htIikgJT4lIA0KICBmaWx0ZXIoTmFtZSA9PSAiTWFjaGluZSAjMSIpICU+JSANCiAgc2VsZWN0KFN0YXJ0RGF0ZSwgQW5hbG9nVmFsKSAlPiUgDQogIGFycmFuZ2UoU3RhcnREYXRlKSANCmhlYWQoREZfTTEpDQpgYGANCg0KIyMjIyBUcmFuc3Bvc2luZyB0aGUgZGF0YQ0KDQpOb3cgd2UgbmVlZCB0byAqKnRyYW5zcG9zZSoqIG91ciBkYXRhIGZyb20gbG9uZyB0byB3aWRlIHN0cnVjdHVyZSBhbmQgdG8gZml0IGl0IHRvIGBtYXRyaXhgISBGb3IgdGhlIG1vbWVudCwgd2Ugd2lsbCAnZm9yZ2V0JyB0aGUgU3RhcnREYXRlIHZhbHVlcyBhbmQgc2ltcGx5IHBhcnNlIHRoZSB2YWx1ZXMgaW50byBtYXRyaXggb2YgZGltZW5zaW9uIHNheSAxNTAgY29sdW1ucyBhbmQgMjAgcm93cy4uLiBPZiBjb3Vyc2Ugb25lIG5lZWQgdG8gcmVjb3ZlciBzb21lIGJhc2ljIFIgc2tpbGxzIGZvciB0aGF0IDopIGlmIG5vdCB1c2Ugc3RhY2tvdmVyZmxvdy4uLiAoSG93IHRvIHR1cm4gYSB2ZWN0b3IgaW50byBhIG1hdHJpeCBpbiBSPylbaHR0cHM6Ly9zdGFja292ZXJmbG93LmNvbS9xdWVzdGlvbnMvMTQ2MTQ5NDYvaG93LXRvLXR1cm4tYS12ZWN0b3ItaW50by1hLW1hdHJpeC1pbi1yXQ0KDQpCdXQgaW4gb3JkZXIgdG8gbWFrZSB0aGluZ3MgbW9yZSBjbGVhciBmb3IgeW91IChhbmQgZm9yIG1lKSBJIHdpbGwgc3RhcnQgc2ltcGxlLiBJIHdpbGwgbGltaXQgdGhlIG91dHB1dCB0byA1MCBwaWVjZXMgb2YgdmFsdWVzIHRvIGhhdmUgbW9yZSBjbGVhciB1bmRlcnN0YW5kaW5nIG9uIHRoZSBwcm9jZXNzIHdlIGRvLiANCg0KKipOT1RFOioqIFBsYXkgd2l0aCB0aGUgY29kZSB5b3Vyc2VsZiB0byBiZXR0ZXIgdW5kZXJzdGFuZCBldmVyeSBzdGVwIQ0KDQpgYGB7cn0NCkRGX00xIDwtIERGX1RFTVAgJT4lIA0KICBmaWx0ZXIoRXZlbnRUZXh0ID09ICJUdWJpbmcgUHJvY2VzcywgcmVzaXN0YW5jZSBPaG0iKSAlPiUgDQogIGZpbHRlcihOYW1lID09ICJNYWNoaW5lICMxIikgJT4lIA0KICBhcnJhbmdlKFN0YXJ0RGF0ZSkgJT4lIA0KICBzZWxlY3QoQW5hbG9nVmFsKSAlPiUgDQogIGhlYWQoNTApICU+JSANCiAgdCgpICU+JSAgIyB0aGlzIGJyaW5ncyB1cyBhIG1hdHJpeA0KICBtYXRyaXgobnJvdyA9IDUsIG5jb2wgPSAxMCwgYnlyb3cgPSBUUlVFKSAjIHRyYW5zZm9ybWluZyB0aGF0IGludG8gbWF0cml4IHNpemUgNXJvd3MgYW5kIDEwY29sdW1ucw0KREZfTTENCmBgYA0KDQpOb3RpY2UgdG8gc2V0IGBieXJvd2AgYXJndW1lbnQgdG8gYFRSVUVgLiBUaGlzIHdheSB3ZSB3aWxsIGtlZXAgdHJhY2sgb24gaG93IG91ciBkYXRhIGFyZSBwb3B1bGF0ZWQuIEV2ZXJ5IHJvdyB3aWxsIGNvbnRhaW4gYWJvdXQgMTUwIG9ic2VydmF0aW9ucyBvZiB0aGUgVGltZS1TZXJpZXMgZGF0YS4gVGhpcyB3aWxsIGJlIGVxdWl2YWxlbnQgb2Yga25vd24gdGltZSBwZXJpb2QuDQoNCkNvbWluZyBiYWNrIHRvIG91ciBkYXRhLiBMZXQncyBjb21wbGV0ZSB0aGlzIHByb2NlZHVyZSBmb3IgZW50aXJlIGRhdGFzZXQuIExldCdzIHVzZSB0aGUgcmVjaXBlIHRvIGRvIGEgZmluYWwgdHJhbnNmb3JtYXRpb246DQoNCmBgYHtyfQ0KREZfTTEgPC0gREZfVEVNUCAlPiUgDQogIGZpbHRlcihFdmVudFRleHQgPT0gIlR1YmluZyBQcm9jZXNzLCByZXNpc3RhbmNlIE9obSIpICU+JSANCiAgZmlsdGVyKE5hbWUgPT0gIk1hY2hpbmUgIzEiKSAlPiUgDQogIGFycmFuZ2UoU3RhcnREYXRlKSAlPiUgDQogIHNlbGVjdChBbmFsb2dWYWwpICU+JSANCiAgaGVhZCgzMDAwKSAlPiUgDQogIHQoKSAlPiUgICMgdGhpcyBicmluZ3MgdXMgYSBtYXRyaXgNCiAgbWF0cml4KG5yb3cgPSAyMCwgbmNvbCA9IDE1MCwgYnlyb3cgPSBUUlVFKSAjIHRyYW5zZm9ybWluZyB0aGF0IGludG8gbWF0cml4IHNpemUgMjAgcm93cyBhbmQgMTUwIGNvbHVtbnMNCmBgYA0KDQpXb25kZXJmdWwsIGxldCdzIHRyeSB0byBzZWUhIG91ciBuZXcgb2JqZWN0IGFzIGFuIGltYWdlISEhDQoNCiMjIyMgRXhwbG9yZSB0aGUgbWF0cml4IGFzIGEgc3VyZmFjZSENCg0KTGV0J3MgdXNlIGBwbG90bHlgIDNEIGdyYXBoIHRvIGV4cGxvcmUgd2hhdCB3ZSBoYXZlIGdvdCENCg0KYGBge3IsIGV2YWw9RkFMU0UsIGluY2x1ZGU9VFJVRX0NCnBsb3RfbHkoeiA9IERGX00xLCB0eXBlID0gInN1cmZhY2UiKQ0KYGBgDQoNCiFbVmFsdWVzIGZyb20gTWFjaGluZSAxIHVzZWQgZm9yIFRyYWluaW5nXVtpZDFdDQoNClRoaXMgc2hvdWxkIGJlIGdvb2QgZW5vdWdoIHRvIGZpdCBvdXIgZGVlcCBsZWFybmluZyBtb2RlbC4NCg0KIyMjIyBHZW5lcmFsaXplIGRhdGEgcHJlcGFyYXRpb24gYXMgYSBmdW5jdGlvbg0KDQpBcyBtciBCaWxsIEdhdGVzIG9uY2Ugc2FpZDogIkkgd291bGQgcmF0aGVyIGhpcmUgYSBsYXp5IHBlcnNvbiBhcyBoZS9zaGUgd2lsbCBmaW5kIHRoZSBlYXNpZXN0IHdheSB0byBzb2x2ZSBhIHByb2JsZW0uLi4iIHdlIG11c3QgYmUgbGF6eSEgRm9yIHRoYXQgSSB3aWxsIGFscmVhZHkgbm93IHRoaW5rIGFib3V0IHdyaXRpbmcgYSBmdW5jdGlvbiB0aGF0IHdpbGwgJ3ByZXBhcmUnIG15IHRlc3QgZGF0YXNldHMuIFRha2UgYSBtb21lbnQgdG8gc3R1ZHkgdGhpczoNCg0KYGBge3J9DQojIGJ1aWxkIGZ1bmN0aW9uIGNvbnZlcnRpbmcgdGltZSBzZXJpZXMgZGF0YSB0byBtYXRyaXgNCnRvX21hdHJpeCA8LSBmdW5jdGlvbih4LCBmaWx0ZXJfRXZlbnQsIGZpbHRlcl9NYWNoaW5lLCBuX2NvbHMpIHsNCiAgIyMjIFBVUlBPU0U6IFRyYW5zZm9ybSBUaW1lIFNlcmllcyBDb2x1bW4gb2YgdGhlIGRhdGFmcmFtZSB0byB0aGUgbWF0cml4DQogICMgICAgICAgICAgICB3aXRoIHNwZWNpZmllZCBudW1iZXIgb2YgY29sdW1ucy4gTnVtYmVyIG9mIHJvd3Mgd2lsbCBiZSBhdXRvbWF0aWNhbGx5DQogICMgICAgICAgICAgICBmb3VuZCBhbmQgcmVtYWluaW5nIGRhdGEgcG9pbnRzIGRpc2NhcmRlZA0KICAjICMgVW5jb21tZW50IHZhcmlhYmxlIHRvIGRlYnVnIGZ1bmN0aW9uDQogICMgZmlsdGVyX0V2ZW50IDwtICJUdWJpbmcgUHJvY2VzcywgcmVzaXN0YW5jZSBPaG0iDQogICMgZmlsdGVyX01hY2hpbmUgPC0gIk1hY2hpbmUgIzQiDQogICMgeCA8LSBERl9URU1QDQogICMgbl9jb2xzIDwtIDE1MA0KICANCiAgIyBnZXQgaW50ZXJtZWRpYXRlIG9iamVjdCBhbmQgZGltZW5zaW9uDQogIFN0ZXAxIDwtIHggJT4lIA0KICAgIGZpbHRlcihFdmVudFRleHQgPT0gZmlsdGVyX0V2ZW50KSAlPiUgDQogICAgZmlsdGVyKE5hbWUgPT0gZmlsdGVyX01hY2hpbmUpICU+JSANCiAgICBhcnJhbmdlKFN0YXJ0RGF0ZSkgJT4lIA0KICAgIHNlbGVjdChBbmFsb2dWYWwpDQogICMgZmluZCBudW1iZXIgb2Ygcm93cyBvZiBkYXRhIGZyYW1lDQogIG5yb3dzIDwtIFN0ZXAxICU+JSBucm93KCkNCiAgIyBmaW5kIHRoZSBudW1iZXIgb2Ygcm93IGluIGEgbWF0cml4IChXaG9sZSBSb3dzKSwgdGhlIHZhbHVlIHdpbGwgaGF2ZSBkZWNpbWFscy4uLg0KICBXTiA8LSBucm93cy9uX2NvbHMNCiAgIyMgZXh0cmFjdCB0aGUgd2hvbGUgbnVtYmVyIHVuY29tbWVudCBmb3IgZGVidWcvdGVzdA0KICAjIFdOIDwtIDE5LjINCiAgIyBXTiA8LSAxOS44DQogIGlmKChXTiAtIHJvdW5kKFdOKSkgPCAwKXtXTiA8LSByb3VuZChXTikgLSAxfSBlbHNlIHtXTiA8LSByb3VuZChXTil9DQogICMgZmluZCBudW1iZXIgb2Ygcm93cyB0byBleHRyYWN0IGRhdGENCiAgbiA8LSBuX2NvbHMgKiBXTg0KICAjIGV4dHJhY3QgcmVsZXZhbnQgbWF0cml4DQogIFN0ZXAyIDwtIFN0ZXAxICU+JSANCiAgICBoZWFkKG4pICU+JSAjb25seSB1c2Ugd2hvbGUgbnVtYmVyIHRvIGF2b2lkIGVycm9ycw0KICAgIHQoKSAlPiUgICMgdGhpcyBicmluZ3MgdXMgYSBtYXRyaXgNCiAgICBtYXRyaXgobnJvdyA9IFdOLCBuY29sID0gbl9jb2xzLCBieXJvdyA9IFRSVUUpICMgdHJhbnNmb3JtaW5nIHRoYXQgaW50byBtYXRyaXggc2l6ZSAyMHgxNTANCiAgIyByZXR1cm4gdGhlIHJlc3VsdCBvZiB0aGUgZnVuY3Rpb24NCiAgcmV0dXJuKFN0ZXAyKQ0KfQ0KYGBgDQoNClRoaXMgZnVuY3Rpb24gY2FuIGRlZmluaXRlbHkgYmUgZWFzaWx5IHJldXNlZCBmb3Igb3VyIFNoaW55QXBwLiBJdCBjYW4gYmUgdXNlZCB0byBnZW5lcmF0ZSBkYXRhc2V0IGZvciBhbnkgb3RoZXIgbWFjaGluZSBldmVudC4gQ29kZSBzbmlwcGV0cyBiZWxvdyB3aWxsIHVzZSB0aGlzIGZ1bmN0aW9uIHRvIGdlbmVyYXRlICdUZXN0JyBkYXRhc2V0cy4NCg0KQS4gRm9yIG1hY2hpbmUgMjoNCg0KYGBge3J9DQpERl9NMiA8LSB0b19tYXRyaXgoREZfVEVNUCwgDQogICAgICAgICAgICAgICAgICAgZmlsdGVyX0V2ZW50ID0gIlR1YmluZyBQcm9jZXNzLCByZXNpc3RhbmNlIE9obSIsDQogICAgICAgICAgICAgICAgICAgZmlsdGVyX01hY2hpbmUgPSAiTWFjaGluZSAjMiIsIA0KICAgICAgICAgICAgICAgICAgIG5fY29scyA9IDE1MCkNCmBgYA0KDQpCLiBGb3IgbWFjaGluZSAzOg0KDQpgYGB7cn0NCkRGX00zIDwtIHRvX21hdHJpeChERl9URU1QLCANCiAgICAgICAgICAgICAgICAgICBmaWx0ZXJfRXZlbnQgPSAiVHViaW5nIFByb2Nlc3MsIHJlc2lzdGFuY2UgT2htIiwNCiAgICAgICAgICAgICAgICAgICBmaWx0ZXJfTWFjaGluZSA9ICJNYWNoaW5lICMzIiwgDQogICAgICAgICAgICAgICAgICAgbl9jb2xzID0gMTUwKQ0KYGBgDQoNCkMuIEZvciBtYWNoaW5lIDQ6DQoNCmBgYHtyfQ0KREZfTTQgPC0gdG9fbWF0cml4KERGX1RFTVAsIA0KICAgICAgICAgICAgICAgICAgIGZpbHRlcl9FdmVudCA9ICJUdWJpbmcgUHJvY2VzcywgcmVzaXN0YW5jZSBPaG0iLA0KICAgICAgICAgICAgICAgICAgIGZpbHRlcl9NYWNoaW5lID0gIk1hY2hpbmUgIzQiLCANCiAgICAgICAgICAgICAgICAgICBuX2NvbHMgPSAxNTApDQpgYGANCg0KQW5kIHdlIGNhbiBhbHNvIHZpc3VhbGl6ZSB0aGF0IHRvIGNvbmZyb250Og0KDQpGb3IgTWFjaGluZSAyDQoNCmBgYHtyLCBldmFsPUZBTFNFLCBpbmNsdWRlPVRSVUV9DQpwbG90X2x5KHogPSBERl9NMiwgdHlwZSA9ICJzdXJmYWNlIikNCmBgYA0KDQohW1ZhbHVlcyBmcm9tIE1hY2hpbmUgMiB1c2VkIGZvciBUZXN0XVtpZDJdDQoNCkZvciBNYWNoaW5lIDMgLSBvdXIgY2FuZGlkYXRlIGZvciBhbm9tYWx5IGRldGVjdGlvbiENCg0KYGBge3IsIGV2YWw9RkFMU0UsIGluY2x1ZGU9VFJVRX0NCnBsb3RfbHkoeiA9IERGX00zLCB0eXBlID0gInN1cmZhY2UiKQ0KYGBgDQoNCiFbVmFsdWVzIGZyb20gTWFjaGluZSAzIHVzZWQgZm9yIFRlc3RdW2lkM10NCg0KRm9yIE1hY2hpbmUgNCAtIEFub3RoZXIgY2FuZGlkYXRlIGZvciBhbm9tYWx5IGRldGVjdGlvbiENCg0KYGBge3IsIGV2YWw9RkFMU0UsIGluY2x1ZGU9VFJVRX0NCnBsb3RfbHkoeiA9IERGX000LCB0eXBlID0gInN1cmZhY2UiKQ0KYGBgDQoNCiFbVmFsdWVzIGZyb20gTWFjaGluZSA0IHVzZWQgZm9yIFRlc3RdW2lkNF0NCg0KIyMjIyBGaXR0aW5nIHRoZSBEZWVwIExlYXJuaW5nIE1vZGVsDQoNCkxhdW5jaCB0aGUgZGVlcCBsZWFybmluZyBtYWNoaW5lIGFnYWluLi4uDQoNCmBgYHtyLCBldmFsPVRSVUUsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0UsIGluY2x1ZGU9VFJVRX0NCiMgdG8gbG9hZCB0aGUgbGlicmFyeQ0KbGlicmFyeShoMm8pDQoNCiMgdG8gaW5pdGlhbGl6ZSB0aGUgJ21hY2hpbmUnDQpsb2NhbEgyTyA9IGgyby5pbml0KCkNCmBgYA0KDQojIyMjIExvYWQgZGF0YXNldHMgdG8gSDJPDQoNClRoZW4gd2Ugd2lsbCBkb3dubG9hZCB0aGUgZGF0YXNldHMgaW50byBoMm8uIFJlbWVtYmVyIEgyTyBpcyBqdXN0IG9wZXJhdGVkIGZyb20gUiBidXQgaXQncyBhIGNvbXB1dGVyIGJlc2lkZXMhDQoNCg0KYGBge3IsIGV2YWw9VFJVRSwgaW5jbHVkZT1UUlVFfQ0KIyBJbXBvcnQgdHJhaW4gZGF0YSBpbnRvIHRoZSBIMk8gY2x1c3Rlcg0KdHJhaW5fTTEgPC0gYXMuaDJvKHggPSBERl9NMSwgZGVzdGluYXRpb25fZnJhbWUgPSAidHJhaW5fTTEiKQ0KDQojIEFsc28gaW1wb3J0IG91ciB0ZXN0IGRhdGFzZXRzIGZvciBNYWNoaW5lcyAyIGFuZCAzLi4uDQp0ZXN0X00yICA8LSBhcy5oMm8oeCA9IERGX00yLCBkZXN0aW5hdGlvbl9mcmFtZSA9ICJ0ZXN0X00yIikNCnRlc3RfTTMgIDwtIGFzLmgybyh4ID0gREZfTTMsIGRlc3RpbmF0aW9uX2ZyYW1lID0gInRlc3RfTTMiKQ0KdGVzdF9NNCAgPC0gYXMuaDJvKHggPSBERl9NNCwgZGVzdGluYXRpb25fZnJhbWUgPSAidGVzdF9NNCIpDQoNCmBgYA0KDQoNCiMjIyMgQnVpbGRpbmcgRGVlcCBMZWFybmluZyBtb2RlbCB3aXRoIGF1dG9lbmNvZGVyDQoNCk5vdywgb25jZSB3ZSBrbm93IGhvdyBvdXIgZGF0YSBsb29rcyBsaWtlIHdlIGNhbiBzdGFydCB0byBkbyBvdXIgQW5vbWFseSBNb2RlbC4NCg0KYGBge3IsIGV2YWw9VFJVRSwgaW5jbHVkZT1UUlVFfQ0KDQojIFRyYWluIGRlZXAgYXV0b2VuY29kZXIgbGVhcm5pbmcgbW9kZWwgb24gIm5vcm1hbCIgDQojIHRyYWluaW5nIGRhdGEsIHkgaWdub3JlZCANCm5vcm1hbGl0eV9tb2RlbCA8LSBoMm8uZGVlcGxlYXJuaW5nKA0KIHggPSBuYW1lcyh0cmFpbl9NMSksIA0KIHRyYWluaW5nX2ZyYW1lID0gdHJhaW5fTTEsIA0KIGFjdGl2YXRpb24gPSAiVGFuaCIsIA0KIGF1dG9lbmNvZGVyID0gVFJVRSwgDQogaGlkZGVuID0gYyg1MCwyMCw1MCksIA0KIHNwYXJzZSA9IFRSVUUsDQogbDEgPSAxZS00LCANCiBlcG9jaHMgPSAxMDApDQpgYGANCg0KIyMjIyBDYWxjdWxhdGUgTVNFIGZyb20gdGhlIHRyYWluIGRhdGFzZXQNCg0KTGV0J3MgdXNlIHRoaXMgbW9kZWwgb24gb3VyIHRyYWluaW5nIGRhdGFzZXQuLi4NCg0KYGBge3IsIGV2YWw9VFJVRSwgaW5jbHVkZT1UUlVFfQ0KIyBjb21wdXRlciBlcnJvciBvZiB0aGUgbW9kZWwNCmgyby5hbm9tYWx5KG5vcm1hbGl0eV9tb2RlbCwgdHJhaW5fTTEpICU+JSBhcy5kYXRhLmZyYW1lKCkgJT4lIHBsb3QudHMoeWxpbSA9IGMoMCwgMTApLCB0eXBlID0gInAiKQ0KYGBgDQoNCiMjIyMgR2V0dGluZyB0byBrbm93IHdoYXQgQUkgc2VlOg0KDQpgYGB7ciwgZXZhbD1UUlVFLCBpbmNsdWRlPVRSVUV9DQojIHZpc3VhbGx5IHNlZSBpdA0KdGVzdF9yZWNvbl9NMSA8LSBoMm8ucHJlZGljdChub3JtYWxpdHlfbW9kZWwsIHRyYWluX00xKSAlPiUgYXMubWF0cml4KCkNCmBgYA0KDQpgYGB7ciwgZXZhbD1GQUxTRSwgaW5jbHVkZT1UUlVFfQ0KcGxvdF9seSh6ID0gdGVzdF9yZWNvbl9NMSwgdHlwZSA9ICJzdXJmYWNlIikNCmBgYA0KDQoNCiMjIyMgRGV0ZWN0IEFub21hbHkgdXNpbmcgTVNFIHZhbHVlDQoNCk5vdyB3ZSB3aWxsIHRyeSB0byB1c2UgdGVzdCBkYXRhc2V0cyBmcm9tICoqbWFjaGluZSAyKiouDQoNCmBgYHtyLCBldmFsPVRSVUUsIGluY2x1ZGU9VFJVRX0NCiMgQ29tcHV0ZSByZWNvbnN0cnVjdGlvbiBlcnJvciB3aXRoIHRoZSBBbm9tYWx5IA0KIyBkZXRlY3Rpb24gYXBwIChNU0UgYmV0d2VlbiBvdXRwdXQgYW5kIGlucHV0IGxheWVycykNCmgyby5hbm9tYWx5KG5vcm1hbGl0eV9tb2RlbCwgdGVzdF9NMikgJT4lICBhcy5kYXRhLmZyYW1lKCkgJT4lIHBsb3QudHMoeWxpbSA9IGMoMCwgMTApLCB0eXBlID0gInAiKQ0KYGBgDQoNCldoYXQgYSBtZXNzIHlvdSB3b3VsZCBzYXkgLSBhbGwgb2JzZXJ2YXRpb25zIGFyZSBhbm9tYWxvdXM/PyBZZXMhIEluIGZhY3QgdGhpcyBpcyBjb3JyZWN0LiBJbmRlZWQsIHNvbWV0aW1lcyBBSSBsZWFkIHRvIHN1Y2ggYSBkaXNjb3Zlcmllcy4uLiBJbiByZWFsaXR5IHdlIG9ic2VydmUgdGhhdCBNYWNoaW5lICMyIGlzIGNvbXBsZXRlbHkgZGlmZmVyZW50IGZyb20gTWFjaGluZSAjMS4uLg0KDQpBbmQgZm9yICoqTWFjaGluZSAzKioNCg0KYGBge3IsIGV2YWw9VFJVRSwgaW5jbHVkZT1UUlVFfQ0KaDJvLmFub21hbHkobm9ybWFsaXR5X21vZGVsLCB0ZXN0X00zKSAlPiUgIGFzLmRhdGEuZnJhbWUoKSAlPiUgcGxvdC50cyh5bGltID0gYygwLCAxMCksIHR5cGUgPSAicCIpDQpgYGANCg0KTWFjaGluZSAzIGhhcyBvbmx5IGFub21hbGllcyBhdCBjZXJ0YWluIHBlcmlvZHMuIFRoZXJlIGFyZSBnZW5lcmFsIGdvb2QgcGVyZm9ybWFuY2Ugd2hpbGUgYXQgYXBwcm94aW1hdGVseSAxMCUgb2YgdGltZSB3ZSBoYXZlIGEgcGVyaW9kcyBvZiBhbm9tYWxvdXMgYmVoYXZpb3VyLi4uDQoNCioqTWFjaGluZSA0KiogWW91IG1heSB0cnkgdG8gcHJhY3RpY2Ugd2l0aCBNYWNoaW5lIDQhDQoNCmBgYHtyLCBldmFsPVRSVUUsIGluY2x1ZGU9VFJVRX0NCmgyby5hbm9tYWx5KG5vcm1hbGl0eV9tb2RlbCwgdGVzdF9NNCkgJT4lICBhcy5kYXRhLmZyYW1lKCkgJT4lIHBsb3QudHMoeWxpbSA9IGMoMCwgMTApLCB0eXBlID0gInAiKQ0KYGBgDQoNCk1hY2hpbmUgNCBzaG93cyBhbm9tYWxpZXMgb25seSBhdCBwZXJpb2RzIDUgYW5kIDguIFRoZW4gdGhlIGJlaGF2aW91cyBzdGFiaWxpemVzLi4uDQoNCiMjIyMgU2F2aW5nIERlZXAgTGVhcm5pbmcgTW9kZWwgZm9yIHRoZSBmdXR1cmUgdXNlDQoNClRvIHVzZSBvdXIgbW9kZWwgaW4gb3VyIFNoaW55QXBwIHdlIHdpbGwgc2F2ZSBpdC4uLg0KDQpgYGB7ciwgZXZhbD1GQUxTRSwgaW5jbHVkZT1UUlVFfQ0KDQppZighZmlsZS5leGlzdHMoInd3dy90bXAvbm9ybWFsaXR5X21vZGVsLmJpbiIpKXsNCmgyby5zYXZlTW9kZWwobm9ybWFsaXR5X21vZGVsLCAid3d3L3RtcC9ub3JtYWxpdHlfbW9kZWwuYmluIikNCmgyby5kb3dubG9hZF9wb2pvKG5vcm1hbGl0eV9tb2RlbCwgInd3dy90bXAiLCBnZXRfamFyID0gVFJVRSkNCn0NCmBgYA0KDQpBbmQgbGV0J3Mgbm90IGZvcmdldCB0byBzd2l0Y2ggb2ZmIG91ciBjbHVzdGVyIQ0KYGBge3IsIGV2YWw9VFJVRSwgaW5jbHVkZT1UUlVFfQ0KaDJvLnNodXRkb3duKHByb21wdD0gRkFMU0UpDQoNCmBgYA0KDQojIyMjIENvbmNsdXNpb24NCg0KaW4gdGhpcyBsZWN0dXJlIHdlIGhhdmUgcGVyZm9ybWVkOg0KDQoqIFRpbWUtU2VyaWVzIGRhdGEgdHJhbnNmb3JtYXRpb24gZnJvbSBEYXRhRnJhbWUgdG8gTWF0cml4DQoqIFRyYWluZWQgQXV0b2VuY29kZXIgbW9kZWwgb24gJ2Jlc3QnIHBlcmZvcm1pbmcgbWFjaGluZQ0KKiBNb2RlbCB3YXMgdGVzdGVkIG9uIGEgJ3dvcnNlJyBwZXJmb3JtaW5nIG1hY2hpbmUNCiogTW9kZWwgd2FzIHNhdmVkIGluIHNldmVyYWwgZm9ybWF0cyBmb3IgdGhlIGZ1cnRoZXIgdXNlDQoNCiMjIyMgTmV4dCBzdGVwDQoNCm91ciBuZXh0IHN0ZXAgd2lsbCBiZSB0byByZXBlYXQgdGhlIHByb2NlZHVyZSBidXQgb24gb3VyIG1hY2hpbmUgZGF0YS4gDQoNCiogcmUtYXJyYW5naW5nIGRhdGEgYXMgbWF0cml4DQoqIGZpdHRpbmcgZGVlcCBsZWFybmluZyBtb2RlbHMNCiogdGVzdGluZyB0aGUgbW9kZWxzDQoqIHNhdmluZyBtb2RlbHMNCiogaW1wbGVtZW50YXRpb24gaW4gb3VyIFNoaW55QXBwLi4uDQoNCiMjIyMgdXNlZCByZWZlcmVuY2VzDQoNCmV4YW1wbGUgZnJvbTogKGh0dHBzOi8vZHpvbmUuY29tL2FydGljbGVzL2Fub21hbHktZGV0ZWN0aW9uLXdpdGgtZGVlcC1sZWFybmluZy1pbi1yLXdpdGgtaDJvKVtodHRwczovL2R6b25lLmNvbS9hcnRpY2xlcy9hbm9tYWx5LWRldGVjdGlvbi13aXRoLWRlZXAtbGVhcm5pbmctaW4tci13aXRoLWgyb10NCg0KTW9yZSByZWFkaW5nOiAoaHR0cHM6Ly9kem9uZS5jb20vYXJ0aWNsZXMvdGhlLWJhc2ljcy1vZi1kZWVwLWxlYXJuaW5nLWhvdy10by1hcHBseS1pdC10by1wcmU/ZnJvbXJlbD10cnVlKVtodHRwczovL2R6b25lLmNvbS9hcnRpY2xlcy90aGUtYmFzaWNzLW9mLWRlZXAtbGVhcm5pbmctaG93LXRvLWFwcGx5LWl0LXRvLXByZT9mcm9tcmVsPXRydWVdDQoNCkFuZDogKGh0dHBzOi8vc2hpcmluZy5naXRodWIuaW8vbWFjaGluZV9sZWFybmluZy8yMDE3LzA1LzAxL2ZyYXVkKVtodHRwczovL3NoaXJpbmcuZ2l0aHViLmlvL21hY2hpbmVfbGVhcm5pbmcvMjAxNy8wNS8wMS9mcmF1ZF0NCg0KcGFwZXI6IChodHRwczovL2FyeGl2Lm9yZy9hYnMvMTcwMS4wMTg4NylbaHR0cHM6Ly9hcnhpdi5vcmcvYWJzLzE3MDEuMDE4ODddDQpJbiB0aGlzIGxlY3R1cmUgd2Ugd291bGQgZXhwbG9yZSB0aGUgJ3RlY2hub2xvZ3knIG9uIHRoZSBzYW1wbGUgYW5kIHRyeSB0byBkbyB0aGlzIGluIDEwIG1pbiBsZWN0dXJlIQ0KDQoNCg0KW2lkMV06IHBsb3RzL00xX3RyYWluLnBuZyAiRGF0YSB1c2VkIHRvIFRyYWluIHRoZSBtb2RlbCINCltpZDJdOiBwbG90cy9NMl90ZXN0LnBuZyAiRGF0YSB1c2VkIHRvIFRlc3QgdGhlIG1vZGVsIC0gTWFjaGluZSAyIg0KW2lkM106IHBsb3RzL00zX3Rlc3QucG5nICJEYXRhIHVzZWQgdG8gVGVzdCB0aGUgbW9kZWwgLSBNYWNoaW5lIDMiDQpbaWQ0XTogcGxvdHMvTTRfdGVzdC5wbmcgIkRhdGEgdXNlZCB0byBUZXN0IHRoZSBtb2RlbCAtIE1hY2hpbmUgNCINCg0K